-
Notifications
You must be signed in to change notification settings - Fork 9
/
FileBrowser.swift
162 lines (145 loc) · 5.31 KB
/
FileBrowser.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//
// FileBrowser.swift
// Chord Provider
//
// © 2024 Nick Berendsen
//
import SwiftUI
import OSLog
import ChordProShared
/// The observable ``FileBrowser`` class
@Observable
class FileBrowser {
/// The list of songs
var songList: [SongItem] = []
/// The list of artists
var artistList: [ArtistItem] = []
/// The name of the folder bookmark
static let folderBookmark: String = "SongsFolder"
/// The name of the export bookmark
static let exportBookmark: String = "ExportFolder"
/// The message of the folder selector
static let message: String = "Select the folder with your songs"
/// The label for the confirmation button of the folder selector
static let confirmationLabel = "Select"
/// The optional songs folder
var songsFolder: URL?
/// The status
var status: AppError = .unknownStatus
/// The list of open windows
var openWindows: [NSWindow.WindowItem] = []
/// The MenuBarExtra window
/// - Note: Needed to close the MenuBarExtra when selecting a song
var menuBarExtraWindow: NSWindow?
/// Init the FileBrowser
init() {
songsFolder = UserFileBookmark.getBookmarkURL(UserFileItem.songsFolder)
}
}
extension FileBrowser {
// MARK: Structures
/// The struct for a song item in the browser
struct SongItem: Identifiable, Equatable {
/// The unique ID
var id: String {
fileURL.description
}
/// Name of the artist
var artist: String = "Unknown artist"
/// Title of the song
var title: String = ""
/// The optional tags
var tags: [String] = []
/// The searchable string
var search: String {
"\(title) \(artist)"
}
/// Path of the optional audio file
var musicPath: String = ""
/// URL of the ChordPro document
var fileURL: URL
}
/// The struct for an artist item in the browser
struct ArtistItem: Identifiable {
/// The unique ID
var id: String { name }
/// Name of the artist
let name: String
/// Songs of the artist
let songs: [SongItem]
}
}
extension FileBrowser {
// MARK: Functions
/// Get the song files from the user selected folder
func getFiles() {
var songs = songList
/// Get a list of all files
if let songsFolder = UserFileBookmark.getBookmarkURL(UserFileItem.songsFolder) {
/// Get access to the URL
_ = songsFolder.startAccessingSecurityScopedResource()
status = .songsFolderIsSelected
if songs.isEmpty, let items = FileManager.default.enumerator(at: songsFolder, includingPropertiesForKeys: nil) {
while let item = items.nextObject() as? URL {
if ChordProDocument.fileExtension.contains(item.pathExtension) {
var song = SongItem(fileURL: item)
FileBrowser.parseSongFile(item, &song)
songs.append(song)
}
}
}
/// Close access to the URL
songsFolder.stopAccessingSecurityScopedResource()
/// Use the Dictionary(grouping:) function so that all the artists are grouped together.
let grouped = Dictionary(grouping: songs) { (occurrence: SongItem) -> String in
occurrence.artist
}
/// We now map over the dictionary and create our artist objects.
/// Then we want to sort them so that they are in the correct order.
artistList = grouped.map { artist -> ArtistItem in
ArtistItem(name: artist.key, songs: artist.value)
}
.sorted { $0.name < $1.name }
songList = songs.sorted { $0.title < $1.title }
} else {
/// There is no folder selected
songsFolder = nil
status = .noSongsFolderSelectedError
}
}
/// Parse the song file for metadata
static func parseSongFile(_ file: URL, _ song: inout SongItem) {
song.title = file.lastPathComponent
do {
let data = try String(contentsOf: file, encoding: .utf8)
for text in data.components(separatedBy: .newlines) where text.starts(with: "{") {
parseFileLine(text: text, song: &song)
}
} catch {
Logger.application.error("\(error.localizedDescription, privacy: .public)")
}
/// Parse the actual metadata
func parseFileLine(text: String, song: inout SongItem) {
if let match = text.wholeMatch(of: ChordPro.directiveRegex) {
let directive = match.1
let label = match.2
switch directive {
case .t, .title:
song.title = label ?? "Unknown Title"
case .st, .subtitle, .artist:
song.artist = label ?? "Unknown Artist"
case .musicPath:
if let label {
song.musicPath = label
}
case .tag:
if let label {
song.tags.append(label.trimmingCharacters(in: .whitespacesAndNewlines))
}
default:
break
}
}
}
}
}